#!/usr/bin/python
#----------------------------------------------------------------------------
# Name:         fPOP.py (temporary)
# Purpose:      A minimal PythonCard POP3 client for spam filtering
#
# Author:       Fred Pacquier
#
# Version:       2002.07.05
# Copyright:    (c) 2002 by Blue Tango Software
# Licence:      PGPL(JK) (Pretty Generous Pacman's License (Just Kiddin'))
#----------------------------------------------------------------------------

"""
__version__ = "$Revision: 1.49 $"
__date__ = "$Date: 2005/12/13 11:13:23 $"
"""

"""
This is intended as a sample for the MultiColumnList some day, though it's heavy on straight wxPython right now

Goal: imitate the Windows utility "nPOP" (hence the temporary code name), with the following differences :

- simplified mail capabilities (one account, list/sort/preview/delete -- no download, storage, posting)
- extended filtering capabilities (auto-flagging, blacklists, whitelists...)

What works in this proto :
 collect/save/read account info & user prefs (WARNING: NO SECURITY :)
 connect to POP, list headers, sort columns, read (raw) mail
 status icons : unread/read/marked for delete
 manual refresh
 keyboard handling (Del key)
 right-click context menu on list (mark and preview are bound)
 intelligent auto-resize of list (adapted from code in the listctrl.py mixin from the wxPython CVS)
 timer for auto-refresh list (at startup only)
 
What doesn't work :
 the list never sees the Enter key (should cause Preview like double-click)
 mark for delete is just a mockup (icon change), there is no server update
 the list is not refreshed while headers are being added
 no error-checking...
 
To Do list :
 proper child window for mail preview
 server update (delete marked)
 actually filter something (implement blacklist/whitelist)
 add icon for whitelisted items
 use ConfigParser instead of eval
 handle Quoted Printable in display
 save window pos/size between sessions
 handle message date/time better
 etc...

Feeback (and/or patches) welcome,
FP

"""

"""
KEA
when you start deleting messages off of the server does that 
change the message numbers already retrieved? for example,
if we have messages 1, 2, 3, 4 and then we delete message
2, will message 3 become 2 and message 4 become 3? if another
message arrives will that one be 5 or 4?

the difference between the self.msgs key, what's displayed in the
list and what's on the server needs to be nailed down in order to
avoid one off errors

see
http://www.tuxedo.org/~esr/fetchmail/fetchmail-FAQ.html
http://www.cs.uu.nl/wais/html/na-dir/mail/mime-faq/.html

I would like to change the blacklist and whitelist handling
so that the email.parseaddr function is used to split the 
string into realname and address parts. I would like to store
the full address on disk, but any comparisons should just be
done against the address part unless it wasn't possible to split
the name/address.
email.dump_address_pair will be used to put together the realname
and address parts.
Maybe blacklist should be renamed to spammerlist?

The program is currently using the From: address for the lists
even though it says "Sender" in the menus... If the code
is changed to actually use the sender field that will probably
be confusing since the list is displaying the From field. But
we probably don't want to have a full set of blacklist and whitelist
options for both the Sender and From addresses, so a simplification
of the UI is necessary. Pondering...
"""

import wx
from PythonCard import configuration, dialog, log, model, timer
import dlg_config
from preview import Preview
from message import Message
import fpoputil

import os, poplib, cStringIO, rfc822, time
import email
from email import Parser
import shutil

# keep in sync with resource file !
numcols = 5

SENDER_INDEX = 5
HEADERS_INDEX = 6
FULL_MESSAGE_INDEX = 7
WHITELIST_FILENAME = 'whitelist.txt'
BLACKLIST_FILENAME = 'blacklist.txt'
SUBJECTLIST_FILENAME = 'subjectlist.txt'
CONFIG_FILE = 'fpop.ini'

class Fpop(model.Background):

    def on_initialize(self, event):
        self.initializeList()

        self.msgs = {}

        self.lastcount = 0
        # provide default configuration values
        self.fromName = ''
        self.fromAttribution = ''
        self.fromAddress = ''
        self.replyAddress = ''
        self.SMTPserver = ''
        self.POPserver = ''
        self.POPuser = ''
        self.POPpwd = ''
        self.CheckAtStart = 0
        self.AutoCheck = 0
        self.AutoCheckInterval = 5
        self.PreviewLines = 100
        # XXX bad security - need to convert to ConfigParser...
        try:
            self.configPath = os.path.join(configuration.homedir, 'fpop')
            if not os.path.exists(self.configPath):
                os.mkdir(self.configPath)
            basePath = self.application.applicationDirectory
            self.inifile = os.path.join(self.configPath, CONFIG_FILE)
            if not os.path.exists(self.inifile):
                shutil.copy2(os.path.join(basePath, CONFIG_FILE), self.inifile)
            execfile(self.inifile)
            if self.POPserver == '' or self.POPuser == '':
                self.components.btnGetHeaders.enabled = 0
        except :
            self.components.btnGetHeaders.enabled = 0

        # KEA 2002-07-10
        # email messages from addresses in the blacklist
        # should be deleted off of the server automatically
        self.whitelist = fpoputil.loadList(os.path.join(self.configPath, WHITELIST_FILENAME))
        self.blacklist = fpoputil.loadList(os.path.join(self.configPath, BLACKLIST_FILENAME))
        self.subjectlist = fpoputil.loadList(os.path.join(self.configPath, SUBJECTLIST_FILENAME))
        #self.whitelist = self.loadWhitelist()

        self.bindMultiColumnListEvents()
        self._needResize = 0
        
        self.initContextMenu()
        self.initSizers()
        
        self.createPreviewWindow()
        self.messageWindows = {}
        
        if self.CheckAtStart :
            self.components.mclHeaders.Refresh()
            self.doGetNewHeaders()
        if self.AutoCheck :
            self.refreshTimer = timer.Timer(self.components.btnGetHeaders, -1)
            self.refreshTimer.start(self.AutoCheckInterval * 60 * 1000)

    # so how much to wrap and how much to leave raw wxPython?
    def initializeList(self):
        listX = self.components.mclHeaders
        listX.InsertColumn(0, "#", wx.LIST_FORMAT_RIGHT)
        listX.InsertColumn(1, "Subject")
        listX.InsertColumn(2, "From") 
        listX.InsertColumn(3, "Date")
        listX.InsertColumn(4, "Size", wx.LIST_FORMAT_RIGHT)
        
        self.imagelist = wx.ImageList(16,16)
        for gif in ('unread.gif', 'read.gif', 'cross.gif', 'question.gif'):
            self.imagelist.Add(wx.Bitmap(gif, wx.BITMAP_TYPE_GIF ))
        listX.SetImageList(self.imagelist, wx.IMAGE_LIST_SMALL)

    # KEA 2002-07-05
    # this should go away once the events are bound
    # in the MultiColumnList component
    def bindMultiColumnListEvents(self):
        mclHeaders = self.components.mclHeaders
        # these bindings have been moved to multicolumnlist.py
        #~ wx.EVT_LIST_ITEM_FOCUSED(self.panel, mclHeaders.id, self.on_mclHeaders_focusChange)
        #wx.EVT_LIST_ITEM_RIGHT_CLICK(self.panel, mclHeaders.id, self.on_mclHeaders_rightClick)
        #wx.EVT_LIST_KEY_DOWN(self.panel, mclHeaders.id, self.on_mclHeaders_keyDown)
        #wx.EVT_LIST_ITEM_ACTIVATED(self.panel, mclHeaders.id, self.on_mclHeaders_itemActivated)
        # KEA 2002-07-16
        # if size and move events are bound to the components
        # it should be done in binding.py for all of them
        wx.EVT_SIZE(mclHeaders, self._onResize)

    def initContextMenu(self):
        self.popup = wx.Menu()
        mID = wx.NewId()
        self.popup.Append(mID, 'Mark for deletion\tDel', 'Flag message to be deleted on server')
        wx.EVT_MENU(self, mID, self.on_markDelete_command)
        mID = wx.NewId()
        self.popup.Append(mID, 'Add Sender to Blacklist', 'Add an entry to the blacklist based on this message')
        wx.EVT_MENU(self, mID, self.on_blacklistAdd_command)
        mID = wx.NewId()
        self.popup.Append(mID, 'Remove Sender from Blacklist', 'Remove an entry from the blacklist based on this message')
        wx.EVT_MENU(self, mID, self.on_blacklistRemove_command)
        mID = wx.NewId()
        self.popup.Append(mID, 'Add Sender to Whitelist', 'Add an entry to the whitelist based on this message')
        wx.EVT_MENU(self, mID, self.on_whitelistAdd_command)
        mID = wx.NewId()
        self.popup.Append(mID, 'Remove Sender from Whitelist', 'Remove an entry from the whitelist based on this message')
        wx.EVT_MENU(self, mID, self.on_whitelistRemove_command)
        mID = wx.NewId()
        self.popup.Append(mID, '&Preview', 'Read this message')
        wx.EVT_MENU(self, mID, self.on_readCurrentMessage_command)

    def initSizers(self):
        sizer1 = wx.BoxSizer(wx.VERTICAL)
        sizer4 = wx.BoxSizer(wx.HORIZONTAL)
        comp = self.components
        
        btnFlags = wx.LEFT | wx.ALIGN_BOTTOM
        vertFlags = wx.LEFT | wx.TOP | wx.ALIGN_LEFT
        fldFlags = wx.RIGHT | wx.ALIGN_BOTTOM

        sizer4.Add(comp.btnGetHeaders, 0, btnFlags, 5)
        #~ sizer4.Add(comp.btnRefresh, 0, btnFlags, 5)
        #~ sizer4.Add(comp.btnWhitelist, 0, btnFlags, 5)
        #~ sizer4.Add(comp.btnPlonk, 0, btnFlags, 5)
        #~ sizer4.Add(comp.btnCommit, 0, btnFlags, 5)
        sizer4.Add((5, 5), 1)  # spacer
        sizer4.Add(comp.btnConfigure, 0, fldFlags, 5)
        sizer4.Add(comp.btnExit, 0, fldFlags, 5)
        sizer1.Add(sizer4, 0, wx.EXPAND)
        sizer1.Add(comp.mclHeaders, 1, wx.EXPAND)
        #~ sizer1.Fit(self)
        sizer1.SetSizeHints(self)
        self.panel.SetSizer(sizer1)
        self.panel.SetAutoLayout(1)
        self.panel.Layout()

    def createPreviewWindow(self):
        self.previewWindow = model.childWindow(self, Preview)
        # override resource position
        self.previewWindow.SetPosition((425, -1))
        #self.previewWindow.visible = True

    def createMessageWindow(self, msg=None):
        messageWindow = model.childWindow(self, Message)
        # override resource position
        messageWindow.SetPosition((125, -1))
        if msg is not None:
            messageWindow.displayMessage(msg)
        messageWindow.visible = True
        # we might need to have a Windows menu
        # to select from all the possible windows?
        self.messageWindows[messageWindow.GetId()] = messageWindow
        return messageWindow

    def on_newMessage_command(self, event):
        # create a new window, but leave the fields blank
        self.createMessageWindow()

    def _onResize(self, event):
        """ Respond to the wxListCtrl being resized.
            Automatically resize the subject and sender columns according to available space.
        """
        self._doResize()
        event.Skip()

    def _doResize(self):
        """ Resize the subject and sender columns to occupy respectively 60% and 40% of the current list width,
            the other columns (index, date, size) being left at their auto-sized values.
        """
        listX = self.components.mclHeaders
        numCols = listX.GetColumnCount()
        if numCols == 0:
            # Nothing to resize
            return
        listWidth = listX.GetSize().width
        if listX.GetItemCount() > listX.GetCountPerPage():
            # We're showing the vertical scrollbar -> allow for scrollbar width
            scrollWidth = wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
            listWidth = listWidth - scrollWidth
        totColWidth = 0 # Sum width of columns index, date & size (after auto-sizing)
        for col in (0, 3,4):
            totColWidth = totColWidth + listX.GetColumnWidth(col)
        # allow for platform-dependant number of extra pixels for correct sizing. Highly empirical.
        if wx.Platform == '__WXMSW__':
            margin = 6
        elif wx.Platform == '__WXGTK__':
            margin = 8
        else:
            margin = 0
        # Resize the subject & sender columns to take up the remaining available space (60/40 ratio)
        leftover = listWidth - totColWidth - margin
        listX.SetColumnWidth(1, int(leftover * .6))
        listX.SetColumnWidth(2, int(leftover * .4))

    def on_btnExit_mouseClick(self, event):
        self.close()

    def doProcessBlacklist(self):
        # this doesn't really process the blacklist
        # just dumps the msgs so we can debug what we have
        for key, value in self.msgs.iteritems():
            print "key", key
            print "value", value

    # KEA 2002-07-05
    # maybe break this up so that the fetch of the headers
    # is separated from updating the on screen list
    # perhaps using a separate thread and Queue to send an event back
    # to be processed on_idle for updating the display
    def doGetNewHeaders(self):
        if self.POPserver == '' or self.POPuser == '':
            return

        # if the user hasn't stored their password
        # then prompt for it and save it in memory
        # while the program is running
        if self.POPpwd == '':
            result = dialog.textEntryDialog(self, 'Enter your password:', 'Password', 
                '', wx.TE_PASSWORD)
            if result.accepted and result.text != '':
                self.POPpwd = result.text
            else:
                return

        listX = self.components.mclHeaders
        self.statusBar.text = 'Connecting to server...'
        try:
            mb = poplib.POP3(self.POPserver)
            mb.user(self.POPuser)
            mb.pass_(self.POPpwd)
            msgnum, totsize = mb.stat()
            # should have better checks to make sure what we're displaying
            # isn't out of sync, possibly using message ids, comparing the number
            # and message id that we have against the last message number and id on the
            # server. of course, if the message counts differ we know we have to reread
            # the messages in which case what do we do with the current mark for deletion
            # info? wait until we have the new messages, then based on message id remark
            # the messages for deletion?
            if msgnum == self.lastcount :
                self.statusBar.text = 'No new messages on server - Total : %d' % msgnum
                return
            if msgnum < self.lastcount :
                listX.DeleteAllItems()
                self.msgs = {}
                self.lastcount=0
            for i in range(self.lastcount, msgnum):
                self.statusBar.text = 'Fetching message %d of %d' % (i+1, msgnum-self.lastcount)
                ##resp, headers, size = mb.top(i+1, 1)
                # i think there is a bug in the top method
                # so pad until the issue is resolved
                resp, headers, size = mb.top(i+1, 25)
                resp, rank, uid = mb.uidl(i+1).split()
                # save headers for later display in preview...
                """
                if i == 21:
                    print headers
                """
                #if there is a blank line in the headers
                #then chop them off there
                try:
                    headers = headers[:headers.index('')]
                except ValueError:
                    print "can't find blank line in", i
                headers = '\n'.join(headers)
                ##m = rfc822.Message(cStringIO.StringIO(headers))
                ##subject = m.get('subject', '(None)')
                # the sender and from can be different
                # for mailing lists you want to add the sender
                # to your whitelist
                ##sender = m.get('sender', '(None)')
                ##fromAddress = m.get('from', '(None)')
                ##date = fpoputil.dateString(m.getdate('date',))
                #tempMessage = email.message_from_string(headers, Parser.HeaderParser())
                tempMessage = Parser.HeaderParser().parse(cStringIO.StringIO(headers))
                subject = tempMessage.get('subject', '(None)')
                sender = tempMessage.get('sender', '(None)')
                fromAddress = tempMessage.get('from', '(None)')
                date = fpoputil.dateString(tempMessage.get('date'))
                """
                if i == 21:
                    print subject
                    print sender
                    print sender
                    print fromAddress
                    print date
                """
    
                # go ahead and list all the messages
                # before marking anything for deletion
                thismsg = (`i+1`, subject, fromAddress, date, `size`)
                listX.InsertImageStringItem(i, thismsg[0], 0)
                for c in range(numcols):
                    listX.SetStringItem(i, c, thismsg[c])
                listX.SetItemData(i, i)
                # the last item in the list is the full message text
                # which is only retrieved for a preview
                # the positions of the items in the list should probably
                # be constants, FULL_MESSAGE_INDEX is the last item
                thismsg = [i+1, subject, fromAddress, date, size, sender, headers, None]
                self.msgs[i] = thismsg
    
                # if a sender is in the blacklist
                # then don't add it to the list?
                # and delete it off of the server
                # probably need a more sophisticated way of matching
                # this should be tested thoroughly before any actual deletes
                # from a server are done to avoid deleting messages that
                # are okay
                # might also check the reply-to header
                if (sender in self.whitelist) or (fromAddress in self.whitelist):
                    pass
                elif (sender in self.blacklist) or (fromAddress in self.blacklist):
                    listX.SetItemImage(i, 2, 2)
                    log.info("message %d from %s would have been deleted" % (i, sender))
                    self.addToSubjectBlacklist(subject)
                    # we still have to add the message to the list right now
                    # because otherwise the list index will be off
                    # so that logic needs to be changed first
                elif fpoputil.filteredSubject(subject) in self.subjectlist:
                    # might want to use a different icon, maybe a cross
                    # with a superimposed ? to signify probable deletion?
                    listX.SetItemImage(i, 3, 3)
                    log.info("message %d from %s is probably SPAM" % (i, sender))
                elif tempMessage.get('message-id') is None:
                    listX.SetItemImage(i, 3, 3)
                    log.info("message %d from %s is probably SPAM, it has no Message-ID" % (i, sender))

            self.statusBar.text = 'Got %d new message(s) - Total : %d' % (msgnum-self.lastcount, msgnum)
            self.lastcount = msgnum
        finally:
            mb.quit()

        # do we actually need to do this association?
        listX.SetItemDataMap(self.msgs)
        for c in (0, 3, 4):
            listX.SetColumnWidth(c, wx.LIST_AUTOSIZE)
        listX.SetItemState(0, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)
        listX.setFocus()
        self._doResize()

        # should we handle each message as it comes in instead?
        #self.doProcessBlacklist()

    def on_btnGetHeaders_timer(self, event):
        self.doGetNewHeaders()

    def on_btnGetHeaders_mouseClick(self, event):
        self.doGetNewHeaders()

    def on_btnConfigure_mouseClick(self, event):
        result = dlg_config.configureDialog(self)
        if result.accepted:
            self.fromName =          result.fromName            
            self.fromAttribution =   result.fromAttribution            
            self.fromAddress =       result.fromAddress            
            self.replyAddress =      result.replyAddress            
            self.SMTPserver =        result.SMTPserver
            self.POPserver =         result.POPserver
            self.POPuser =           result.POPuser
            self.POPpwd =            result.POPpwd
            self.CheckAtStart =      result.CheckAtStart
            self.AutoCheck =         result.AutoCheck
            self.AutoCheckInterval = result.AutoCheckInterval 
            self.PreviewLines =      result.PreviewLines

            if self.POPserver == '' or self.POPuser == '':
                self.components.btnGetHeaders.enabled = 0
            else:
                self.components.btnGetHeaders.enabled = 1

            open(self.inifile, 'w').write("""
self.fromName = '%s'
self.fromAttribution = '%s'
self.fromAddress = '%s'
self.replyAddress = '%s'
self.SMTPserver = '%s'
self.POPserver = '%s'
self.POPuser = '%s'
self.POPpwd = '%s'
self.CheckAtStart = %d
self.AutoCheck = %d
self.AutoCheckInterval = %d
self.PreviewLines = %d
""" % (self.fromName, self.fromAttribution, self.fromAddress, self.replyAddress, \
self.SMTPserver, \
self.POPserver, self.POPuser, self.POPpwd, \
self.CheckAtStart, self.AutoCheck, self.AutoCheckInterval, self.PreviewLines) )

    # the currentItem is now retrieved as needed
    # to avoid errors from sorting, deleting items from the list...
    # see getSelection and getCurrentItem
    # to make sure I haven't changed the basic logic
    def on_mclHeaders_select(self, event):
        #self.currentItem = event.m_itemIndex
        pass

#    def on_mclHeaders_mouseDoubleClick(self, event):
# problem : this does not generate 'select' or 'focused' events if the item is already selected, and does not provide GetIndex
# so if the item's position in the list changes between select & doubleclick (eg on sort), currentItem will be wrong...
# ITEM_ACTIVATED seems to work better (it should also handle Enter as well as doubleclick)
    def on_mclHeaders_itemActivated(self, event):
        #self.currentItem = event.GetIndex()
        self.on_readCurrentMessage_command(None)

    #def on_mclHeaders_itemFocused(self, event):
    #    print "focus change", event

    def on_mclHeaders_mouseContextClick(self, event):
        #self.currentItem = event.GetIndex()
        self.PopupMenu(self.popup, event.GetPoint())

    def on_mclHeaders_keyDown(self, event):
        kc = event.keyCode
        print "keyDown:", kc
        if kc == 127:
            self.on_markDelete_command(None)
        event.skip()

    # KEA 2002-07-20
    # experimental marked item deletion
    def deleteMarkedItems(self):
        listX = self.components.mclHeaders
        numMessages = listX.GetItemCount() - 1
        try:
            mb = poplib.POP3(self.POPserver)
            mb.user(self.POPuser)
            mb.pass_(self.POPpwd)

            for i in range(numMessages, -1, -1):
                marked = listX.GetItem(i).m_image == 2
                msg = self.msgs[i]
                serverMessageNumber = msg[0]
                print "item %d: message #%d, marked %d" % (i, serverMessageNumber, marked)
                if marked:
                    print msg[1], msg[2], msg[3], msg[4]
                    del self.msgs[i]
                    listX.DeleteItem(i)
                    mb.dele(serverMessageNumber)
        finally:
            mb.quit()

    def on_markDelete_command(self, event):
        selection = self.getSelection()
        listX = self.components.mclHeaders
        if listX.GetItem(selection).m_image == 2 :
            listX.SetItemImage(selection, 0, 0)
        else:
            listX.SetItemImage(selection, 2, 2)

    def addToSubjectBlacklist(self, subject):
        subject = fpoputil.filteredSubject(subject)
        if subject is None or subject == '(None)' or subject == '':
            return

        if subject in self.subjectlist:
            log.info("already in subjectlist: %s" % subject)
        else:
            self.subjectlist[subject] = None
            log.info("added to subjectlist: %s" % subject)

    # see helper methods in wxPython\controls2.py
    # what is the difference between an item being focused
    # and an item being selected?
    # it looks like we want the first selected item
    def getSelection(self):
        listX = self.components.mclHeaders
        #print "focused %d, selected %d" % \
        #    (listX.GetFocusedItem(), listX.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED))
        return listX.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)

    # return a 0 based key to a message is self.msgs
    # this is different than the current selection
    # depending on the sorting...
    # and the message number on the pop server
    # is different too, that is item 0 of the value
    # of given self.msgs record
    def getCurrentItem(self):
        listX = self.components.mclHeaders
        selection = self.getSelection()
        i = int(listX.GetItemText(selection)) - 1
        #print "selection: %d, item - 1: %d" % (selection, i)
        #print self.msgs[i]
        return i
        
    # could also have a subject blacklist or graylist
    # and automatically add the subject from a blacklist
    # from address
    #
    # can we color code the rows of the list?
    # should we make sure that an address doesn't appear in both the
    # blacklist and whitelist?
    def on_blacklistAdd_command(self, event):
        currentItem = self.getCurrentItem()
        sender = self.msgs[currentItem][2]
        if sender in self.blacklist:
            log.info("already in blacklist: %s" % sender)
        else:
            self.blacklist[sender] = None
            log.info("added to blacklist: %s" % sender)
            # should we change the icon to indicate the sender
            # has been blacklisted or just go ahead and delete
            # this and any other messages from this sender?
        self.addToSubjectBlacklist(self.msgs[currentItem][1])
        # now mark the message as deleted
        selection = self.getSelection()
        listX = self.components.mclHeaders
        listX.SetItemImage(selection, 2, 2)

    def on_blacklistRemove_command(self, event):
        currentItem = self.getCurrentItem()
        sender = self.msgs[currentItem][2]
        if sender in self.blacklist:
            del self.blacklist[sender]
            log.info("removed from blacklist: %s" % sender)
        else:
            log.info("not in blacklist: %s" % sender)

    def on_whitelistAdd_command(self, event):
        currentItem = self.getCurrentItem()
        sender = self.msgs[currentItem][2]
        if sender in self.whitelist:
            log.info("already in whitelist: %s" % sender)
        else:
            self.whitelist[sender] = None
            log.info("added to whitelist: %s" % sender)

    def on_whitelistRemove_command(self, event):
        currentItem = self.getCurrentItem()
        sender = self.msgs[currentItem][2]
        if sender in self.whitelist:
            del self.whitelist[sender]
            log.info("removed from whitelist: %s" % sender)
        else:
            log.info("not in whitelist: %s" % sender)

    # KEA 2002-07-05
    # probably need an option to reuse existing preview window
    # or open a new one
    # preview window should have
    # From:
    # To:
    # Cc:
    # Subject:
    # as StaticText with the contents as non-editable TextField
    # buttons for Reply, Reply All, Forward, Delete, White List, Black List
    # Previous Message, Next Message
    # Display headers would show all of the headers
    # if there is a menubar, it should have Find and Find Next
    # Message Body won't be editable
    def doReadMessage(self, index):
        # when reading a message, why not cache it in a dictionary or list
        # and then if subsequent reads are done it can just be loaded out of the message
        # cache rather than going to the server again
        # KEA 2002-07-10
        # that's what I've implemented below
        # so the PreviewLines is no longer used
        # you can't create a valid email message if you only
        # have part of the text
        if self.msgs[index - 1][FULL_MESSAGE_INDEX] is None:
            self.statusBar.text = 'Connecting to server...'
            mb = poplib.POP3(self.POPserver)
            mb.user(self.POPuser)
            mb.pass_(self.POPpwd)
            self.statusBar.text = 'Retrieving message #%d...' % index
            #msg = mb.top(index, self.PreviewLines)
            # save message for later in case the user does another preview
            msg = mb.top(index, -1)
            mb.quit()
            text = '\n'.join(msg[1])
            self.msgs[index - 1][FULL_MESSAGE_INDEX] = text
        else:
            text = self.msgs[index - 1][FULL_MESSAGE_INDEX]

        # XXX needs to be turned into a proper child window...
        ##dialog.scrolledMessageDialog(self, text, 'Message preview')
        # KEA 2002-07-08
        # so now how do we split this up into the To, Cc, Bcc, Subject, etc.
        # parts so a better Preview window can be created?
        # also need to be able to set the fonts used and remember
        # the user set position and size for the preview window
        # might also need an option for whether to reuse the preview
        # window or create a new one per message, maybe Ctrl-Click would
        # spawn a new one
        # regardless it brings up an issue of window management if there
        # is more than one
        self.previewWindow.visible = True

        # normally, we would probably already have the message around
        # as an object
        try:
            msg = email.message_from_string(text)
            self.previewWindow.displayMessage(msg, index - 1, self.msgs[index - 1][HEADERS_INDEX])
        except TypeError:
            print "\n\n *** conversion failed, here's the message ***\n" + \
            " *** it will also be written to badmessage.txt ***"
            print "----message----"
            print text, "\n\n"
            f = open('badmessage.txt', 'wb')
            f.write(text)
            f.close()
        self.statusBar.text = ''
        
    def on_readCurrentMessage_command(self, event):
        listX = self.components.mclHeaders
        selection = self.getSelection()
        currentItem = self.getCurrentItem()
        self.doReadMessage(self.msgs[currentItem][0])
        if listX.GetItem(selection).m_image == 0 :
            listX.SetItemImage(selection, 1, 1)
        # KEA 2002-07-05
        # I don't think the skip is needed
        #event.skip()

    def on_close(self, event):
        if hasattr(self, 'refreshTimer'):
            self.refreshTimer.stop()
        self.popup.Destroy()
        fpoputil.saveList(os.path.join(self.configPath, WHITELIST_FILENAME), self.whitelist)
        fpoputil.saveList(os.path.join(self.configPath, BLACKLIST_FILENAME), self.blacklist)
        fpoputil.saveList(os.path.join(self.configPath, SUBJECTLIST_FILENAME), self.subjectlist)
        log.info("closing time !")
        event.skip()


if __name__ == '__main__':
    app = model.Application(Fpop)
    app.MainLoop()
